package cc.unitmesh.devins.idea.renderer

import cc.unitmesh.agent.plan.AgentPlan
import cc.unitmesh.agent.plan.MarkdownPlanParser
import cc.unitmesh.agent.render.BaseRenderer
import cc.unitmesh.devins.llm.MessageRole
import cc.unitmesh.agent.render.RendererUtils

import cc.unitmesh.agent.render.TaskInfo
import cc.unitmesh.agent.render.TaskStatus
import cc.unitmesh.agent.render.TimelineItem
import cc.unitmesh.agent.render.ToolCallDisplayInfo
import cc.unitmesh.agent.render.ToolCallInfo
import cc.unitmesh.agent.tool.ToolType
import cc.unitmesh.agent.tool.toToolType
import cc.unitmesh.devins.llm.Message
import cc.unitmesh.llm.compression.TokenInfo
import com.intellij.openapi.diagnostic.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

private val jewelRendererLogger = Logger.getInstance("JewelRenderer")

/**
 * Jewel-compatible Renderer for IntelliJ IDEA plugin.
 *
 * Uses Kotlin StateFlow instead of Compose mutableStateOf to avoid
 * ClassLoader conflicts with IntelliJ's bundled Compose runtime.
 *
 * Implements CodingAgentRenderer interface from mpp-core.
 *
 * This renderer is designed for maximum reusability and can be used with
 * any Compose-based UI (Jewel, Material Design, etc.) as it only exposes
 * StateFlow-based state.
 */
class JewelRenderer : BaseRenderer() {

    // Timeline of all events (messages, tool calls, results)
    private val _timeline = MutableStateFlow<List<TimelineItem>>(emptyList())
    val timeline: StateFlow<List<TimelineItem>> = _timeline.asStateFlow()

    // Current streaming output from LLM
    private val _currentStreamingOutput = MutableStateFlow("")
    val currentStreamingOutput: StateFlow<String> = _currentStreamingOutput.asStateFlow()

    // Processing state
    private val _isProcessing = MutableStateFlow(false)
    val isProcessing: StateFlow<Boolean> = _isProcessing.asStateFlow()

    // Iteration tracking
    private val _currentIteration = MutableStateFlow(0)
    val currentIteration: StateFlow<Int> = _currentIteration.asStateFlow()

    private val _maxIterations = MutableStateFlow(100)
    val maxIterations: StateFlow<Int> = _maxIterations.asStateFlow()

    // Current active tool call
    private val _currentToolCall = MutableStateFlow<ToolCallInfo?>(null)
    val currentToolCall: StateFlow<ToolCallInfo?> = _currentToolCall.asStateFlow()

    // Error state
    private val _errorMessage = MutableStateFlow<String?>(null)
    val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()

    // Task completion state
    private val _taskCompleted = MutableStateFlow(false)
    val taskCompleted: StateFlow<Boolean> = _taskCompleted.asStateFlow()

    // Token tracking
    private val _totalTokenInfo = MutableStateFlow(TokenInfo())
    val totalTokenInfo: StateFlow<TokenInfo> = _totalTokenInfo.asStateFlow()

    private var _lastMessageTokenInfo: TokenInfo? = null

    // Execution timing
    private var executionStartTime = 0L

    private val _currentExecutionTime = MutableStateFlow(0L)
    val currentExecutionTime: StateFlow<Long> = _currentExecutionTime.asStateFlow()

    // Plan tracking (from plan management tool)
    private val _currentPlan = MutableStateFlow<AgentPlan?>(null)
    val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow()

    // Task tracking (from task-boundary tool)
    private val _tasks = MutableStateFlow<List<TaskInfo>>(emptyList())
    val tasks: StateFlow<List<TaskInfo>> = _tasks.asStateFlow()

    /**
     * Set the current plan directly.
     * Used to sync with PlanStateService from CodingAgent.
     */
    fun setPlan(plan: AgentPlan?) {
        jewelRendererLogger.info("setPlan: plan=${plan != null}, tasks=${plan?.tasks?.size ?: 0}")
        _currentPlan.value = plan
    }

    // BaseRenderer implementation

    override fun renderIterationHeader(current: Int, max: Int) {
        _currentIteration.value = current
        _maxIterations.value = max
    }

    override fun renderLLMResponseStart() {
        super.renderLLMResponseStart()
        _currentStreamingOutput.value = ""
        _isProcessing.value = true

        if (executionStartTime == 0L) {
            executionStartTime = System.currentTimeMillis()
        }
        _currentExecutionTime.value = System.currentTimeMillis() - executionStartTime
    }

    override fun renderLLMResponseChunk(chunk: String) {
        reasoningBuffer.append(chunk)

        // Wait for more content if we detect an incomplete devin block
        if (hasIncompleteDevinBlock(reasoningBuffer.toString())) {
            return
        }

        // Filter devin blocks and output clean content
        val processedContent = filterDevinBlocks(reasoningBuffer.toString())
        val cleanContent = cleanNewlines(processedContent)

        _currentStreamingOutput.value = cleanContent
    }

    override fun renderLLMResponseEnd() {
        super.renderLLMResponseEnd()

        val content = _currentStreamingOutput.value.trim()
        if (content.isNotEmpty()) {
            addTimelineItem(
                TimelineItem.MessageItem(
                    role = MessageRole.ASSISTANT,
                    content = content,
                    tokenInfo = _lastMessageTokenInfo
                )
            )
        }

        _currentStreamingOutput.value = ""
        _isProcessing.value = false
        _lastMessageTokenInfo = null
    }

    override fun renderToolCall(toolName: String, paramsStr: String) {
        jewelRendererLogger.info("renderToolCall: toolName=$toolName, paramsStr length=${paramsStr.length}")

        val toolInfo = formatToolCallDisplay(toolName, paramsStr)
        val params = parseParamsString(paramsStr)
        val toolType = toolName.toToolType()

        jewelRendererLogger.info("renderToolCall: parsed params keys=${params.keys}")

        // Handle task-boundary tool - update task list
        if (toolName == "task-boundary") {
            updateTaskFromToolCall(params)
        }

        // Handle plan management tool - update plan state
        if (toolName == "plan") {
            jewelRendererLogger.info("renderToolCall: detected plan tool, calling updatePlanFromToolCall")
            updatePlanFromToolCall(params)
            // Skip rendering plan tool to timeline - it's shown in PlanSummaryBar
            return
        }

        renderToolCallInternal(toolName, toolInfo, params, paramsStr, toolType)
    }

    /**
     * Render a tool call with parsed parameters.
     * This is the preferred method as it avoids string parsing issues with complex values.
     */
    override fun renderToolCallWithParams(toolName: String, params: Map<String, Any>) {
        // Convert params to string format for display
        val paramsStr = params.entries.joinToString(" ") { (key, value) ->
            "$key=\"$value\""
        }
        val toolInfo = formatToolCallDisplay(toolName, paramsStr)
        val toolType = toolName.toToolType()

        // Convert Map<String, Any> to Map<String, String> for internal use
        val stringParams = params.mapValues { it.value.toString() }

        // Handle task-boundary tool - update task list
        if (toolName == "task-boundary") {
            updateTaskFromToolCall(stringParams)
        }

        // Handle plan management tool - update plan state with original params
        if (toolName == "plan") {
            updatePlanFromToolCallWithAnyParams(params)
            // Skip rendering plan tool to timeline - it's shown in PlanSummaryBar
            return
        }

        renderToolCallInternal(toolName, toolInfo, stringParams, paramsStr, toolType)
    }

    /**
     * Internal method to render tool call UI elements
     */
    private fun renderToolCallInternal(
        toolName: String,
        toolInfo: ToolCallDisplayInfo,
        params: Map<String, String>,
        paramsStr: String,
        toolType: ToolType?
    ) {
        // Skip adding ToolCallItem for Shell - will be replaced by LiveTerminalItem
        // This prevents the "two bubbles" problem
        if (toolType == ToolType.Shell) {
            _currentToolCall.value = ToolCallInfo(
                toolName = toolInfo.toolName,
                description = toolInfo.description,
                details = toolInfo.details
            )
            return // Don't add ToolCallItem, wait for addLiveTerminal
        }

        // Extract file path for read/write operations
        val filePath = when (toolType) {
            ToolType.ReadFile, ToolType.WriteFile -> params["path"]
            else -> null
        }

        _currentToolCall.value = ToolCallInfo(
            toolName = toolInfo.toolName,
            description = toolInfo.description,
            details = toolInfo.details
        )

        addTimelineItem(
            TimelineItem.ToolCallItem(
                toolName = toolInfo.toolName,
                description = toolInfo.description,
                params = toolInfo.details ?: paramsStr,
                fullParams = paramsStr,
                filePath = filePath,
                toolType = toolType,
                success = null,
                summary = null,
                output = null,
                fullOutput = null,
                executionTimeMs = null
            )
        )
    }

    /**
     * Update plan state from plan management tool call (string params version)
     */
    private fun updatePlanFromToolCall(params: Map<String, String>) {
        val action = params["action"]?.uppercase() ?: return
        val planMarkdown = params["planMarkdown"] ?: ""
        val taskIndex = params["taskIndex"]?.toIntOrNull()
        val stepIndex = params["stepIndex"]?.toIntOrNull()

        updatePlanState(action, planMarkdown, taskIndex, stepIndex)
    }

    /**
     * Update plan state from plan management tool call with Any params.
     * This is the preferred method as it handles complex values correctly.
     */
    private fun updatePlanFromToolCallWithAnyParams(params: Map<String, Any>) {
        val action = (params["action"] as? String)?.uppercase() ?: return
        val planMarkdown = params["planMarkdown"] as? String ?: ""
        val taskIndex = when (val v = params["taskIndex"]) {
            is Number -> v.toInt()
            is String -> v.toIntOrNull()
            else -> null
        }
        val stepIndex = when (val v = params["stepIndex"]) {
            is Number -> v.toInt()
            is String -> v.toIntOrNull()
            else -> null
        }

        updatePlanState(action, planMarkdown, taskIndex, stepIndex)
    }

    /**
     * Internal method to update plan state
     */
    private fun updatePlanState(action: String, planMarkdown: String, taskIndex: Int?, stepIndex: Int?) {
        jewelRendererLogger.info("updatePlanState: action=$action, planMarkdown length=${planMarkdown.length}")
        when (action) {
            "CREATE", "UPDATE" -> {
                if (planMarkdown.isNotBlank()) {
                    try {
                        val plan = MarkdownPlanParser.parseToPlan(planMarkdown)
                        jewelRendererLogger.info("Parsed plan: ${plan.tasks.size} tasks")
                        _currentPlan.value = plan
                    } catch (e: Exception) {
                        jewelRendererLogger.warn("Failed to parse plan markdown", e)
                        // Keep previous valid plan on parse failure
                    }
                }
            }
            "COMPLETE_STEP" -> {
                if (taskIndex == null || stepIndex == null) return
                _currentPlan.value?.let { plan ->
                    if (taskIndex in 1..plan.tasks.size) {
                        val task = plan.tasks[taskIndex - 1]
                        if (stepIndex in 1..task.steps.size) {
                            val step = task.steps[stepIndex - 1]
                            step.complete()
                            task.updateStatusFromSteps()
                            // Trigger recomposition by creating a new plan instance
                            _currentPlan.value = plan.copy(updatedAt = System.currentTimeMillis())
                        }
                    }
                }
            }
            "FAIL_STEP" -> {
                if (taskIndex == null || stepIndex == null) return
                _currentPlan.value?.let { plan ->
                    if (taskIndex in 1..plan.tasks.size) {
                        val task = plan.tasks[taskIndex - 1]
                        if (stepIndex in 1..task.steps.size) {
                            val step = task.steps[stepIndex - 1]
                            step.fail()
                            task.updateStatusFromSteps()
                            _currentPlan.value = plan.copy(updatedAt = System.currentTimeMillis())
                        }
                    }
                }
            }
            // VIEW action doesn't modify state
        }
    }

    /**
     * Update task list from task-boundary tool call
     */
    private fun updateTaskFromToolCall(params: Map<String, String>) {
        val taskName = params["taskName"] ?: return
        val statusStr = params["status"] ?: "WORKING"
        val summary = params["summary"] ?: ""
        val status = TaskStatus.fromString(statusStr)

        _tasks.update { currentTasks ->
            val existingIndex = currentTasks.indexOfFirst { it.taskName == taskName }
            if (existingIndex >= 0) {
                // Update existing task
                currentTasks.toMutableList().apply {
                    this[existingIndex] = currentTasks[existingIndex].copy(
                        status = status,
                        summary = summary,
                        timestamp = System.currentTimeMillis()
                    )
                }
            } else {
                // Add new task
                currentTasks + TaskInfo(
                    taskName = taskName,
                    status = status,
                    summary = summary
                )
            }
        }
    }

    /**
     * Handle task-boundary tool call to update task progress display.
     * Overrides the interface method to provide UI-specific task tracking.
     */
    override fun handleTaskBoundary(taskName: String, status: String, summary: String) {
        updateTaskFromToolCall(mapOf(
            "taskName" to taskName,
            "status" to status,
            "summary" to summary
        ))
    }

    override fun renderToolResult(
        toolName: String,
        success: Boolean,
        output: String?,
        fullOutput: String?,
        metadata: Map<String, String>
    ) {
        // Capture current tool call info before clearing
        val currentToolCallInfo = _currentToolCall.value
        _currentToolCall.value = null

        val summary = formatToolResultSummary(toolName, success, output)
        val toolType = toolName.toToolType()
        val executionTime = metadata["execution_time_ms"]?.toLongOrNull()

        // For shell commands, check if it's a live session
        val isLiveSession = metadata["isLiveSession"] == "true"
        val sessionId = metadata["sessionId"]
        val liveExitCode = metadata["exit_code"]?.toIntOrNull()

        if (toolType == ToolType.Shell && output != null) {
            val exitCode = liveExitCode ?: (if (success) 0 else 1)
            val executionTimeMs = executionTime ?: 0L
            val command = currentToolCallInfo?.details?.removePrefix("Executing: ") ?: "unknown"

            if (isLiveSession && sessionId != null) {
                // Update the existing LiveTerminalItem with completion status
                _timeline.update { items ->
                    items.map { item ->
                        if (item is TimelineItem.LiveTerminalItem && item.sessionId == sessionId) {
                            item.copy(
                                exitCode = exitCode,
                                executionTimeMs = executionTimeMs,
                                output = fullOutput ?: output
                            )
                        } else {
                            item
                        }
                    }
                }
            } else {
                // Replace the last tool call with terminal output
                _timeline.update { items ->
                    val lastItem = items.lastOrNull()
                    if (lastItem is TimelineItem.ToolCallItem && lastItem.toolType == ToolType.Shell) {
                        items.dropLast(1)
                    } else {
                        items
                    }
                }
                addTimelineItem(
                    TimelineItem.TerminalOutputItem(
                        command = command,
                        output = fullOutput ?: output,
                        exitCode = exitCode,
                        executionTimeMs = executionTimeMs
                    )
                )
            }
        } else {
            // Update the last tool call item with result
            _timeline.update { items ->
                items.mapIndexed { index, item ->
                    if (index == items.lastIndex && item is TimelineItem.ToolCallItem && item.success == null) {
                        item.copy(
                            success = success,
                            summary = summary,
                            output = if (success && output != null) {
                                when (toolName) {
                                    "glob", "grep" -> output
                                    "docql" -> output
                                    else -> if (output.length <= 2000) output else "${output.take(2000)}...\n[Output truncated]"
                                }
                            } else null,
                            fullOutput = fullOutput ?: output,
                            executionTimeMs = executionTime
                        )
                    } else {
                        item
                    }
                }
            }
        }
    }

    override fun renderTaskComplete(executionTimeMs: Long, toolsUsedCount: Int) {
        _taskCompleted.value = true
        _isProcessing.value = false

        // Add a completion message with execution time and tool usage to the timeline
        val parts = mutableListOf<String>()

        if (executionTimeMs > 0) {
            val seconds = executionTimeMs / 1000.0
            val rounded = (seconds * 100).toLong() / 100.0
            parts.add("${rounded}s")
        }

        if (toolsUsedCount > 0) {
            parts.add("$toolsUsedCount tools")
        }

        if (parts.isNotEmpty()) {
            _timeline.update { items ->
                items + TimelineItem.MessageItem(
                    message = Message(
                        role = MessageRole.ASSISTANT,
                        content = "✓ Task marked as complete (${parts.joinToString(", ")})"
                    )
                )
            }
        }
    }

    override fun renderFinalResult(success: Boolean, message: String, iterations: Int) {
        addTimelineItem(
            TimelineItem.TaskCompleteItem(
                success = success,
                message = message,
                iterations = iterations
            )
        )
        _isProcessing.value = false
        executionStartTime = 0L
    }

    override fun renderError(message: String) {
        _errorMessage.value = message
        addTimelineItem(TimelineItem.ErrorItem(message))
        _isProcessing.value = false
    }

    override fun renderRepeatWarning(toolName: String, count: Int) {
        val warning = "Tool '$toolName' called repeatedly ($count times)"
        addTimelineItem(TimelineItem.ErrorItem(warning))
    }

    override fun renderRecoveryAdvice(recoveryAdvice: String) {
        addTimelineItem(
            TimelineItem.MessageItem(
                role = MessageRole.ASSISTANT,
                content = "Recovery Advice:\n$recoveryAdvice"
            )
        )
    }

    override fun renderUserConfirmationRequest(toolName: String, params: Map<String, Any>) {
        addTimelineItem(
            TimelineItem.MessageItem(
                role = MessageRole.SYSTEM,
                content = "Confirmation required for tool: $toolName"
            )
        )
    }

    override fun updateTokenInfo(tokenInfo: TokenInfo) {
        _lastMessageTokenInfo = tokenInfo
        _totalTokenInfo.update { current ->
            TokenInfo(
                totalTokens = current.totalTokens + tokenInfo.totalTokens,
                inputTokens = current.inputTokens + tokenInfo.inputTokens,
                outputTokens = current.outputTokens + tokenInfo.outputTokens,
                timestamp = tokenInfo.timestamp
            )
        }
    }

    // Public methods for UI interaction

    fun addUserMessage(content: String) {
        addTimelineItem(
            TimelineItem.MessageItem(
                role = MessageRole.USER,
                content = content
            )
        )
    }

    fun clearTimeline() {
        _timeline.value = emptyList()
        _currentStreamingOutput.value = ""
        _isProcessing.value = false
        _currentIteration.value = 0
        _errorMessage.value = null
        _taskCompleted.value = false
        _totalTokenInfo.value = TokenInfo()
        _lastMessageTokenInfo = null
        executionStartTime = 0L
        _currentExecutionTime.value = 0L
    }

    fun clearError() {
        _errorMessage.value = null
    }

    fun reset() {
        clearTimeline()
    }

    /**
     * Force stop all processing states.
     * Used when user cancels a task.
     */
    fun forceStop() {
        val currentOutput = _currentStreamingOutput.value.trim()
        if (currentOutput.isNotEmpty()) {
            addTimelineItem(
                TimelineItem.MessageItem(
                    role = MessageRole.ASSISTANT,
                    content = "$currentOutput\n\n[Interrupted]"
                )
            )
        }

        _isProcessing.value = false
        _currentStreamingOutput.value = ""
        _currentToolCall.value = null
    }

    private fun addTimelineItem(item: TimelineItem) {
        _timeline.update { it + item }
    }

    private fun formatToolCallDisplay(toolName: String, paramsStr: String) =
        RendererUtils.formatToolCallDisplay(toolName, paramsStr)

    private fun formatToolResultSummary(toolName: String, success: Boolean, output: String?) =
        RendererUtils.formatToolResultSummary(toolName, success, output)

    private fun parseParamsString(paramsStr: String) =
        RendererUtils.parseParamsString(paramsStr)

    // ========== Live Terminal Support ==========

    // Channel map for awaiting session results
    private val sessionResultChannels = mutableMapOf<String, kotlinx.coroutines.channels.Channel<cc.unitmesh.agent.tool.ToolResult>>()

    /**
     * Adds a live terminal session to the timeline.
     * This is called when a Shell tool is executed with live output support.
     *
     * Note: renderToolCall() skips adding ToolCallItem for Shell tools,
     * so we just add the LiveTerminalItem directly without replacement logic.
     */
    override fun addLiveTerminal(
        sessionId: String,
        command: String,
        workingDirectory: String?,
        ptyHandle: Any?
    ) {
        addTimelineItem(
            TimelineItem.LiveTerminalItem(
                sessionId = sessionId,
                command = command,
                workingDirectory = workingDirectory,
                ptyHandle = ptyHandle
            )
        )
    }

    /**
     * Update the status of a live terminal session when it completes.
     * This is called from the background monitoring coroutine in ToolOrchestrator.
     */
    override fun updateLiveTerminalStatus(
        sessionId: String,
        exitCode: Int,
        executionTimeMs: Long,
        output: String?,
        cancelledByUser: Boolean
    ) {
        // Find and update the LiveTerminalItem in the timeline
        _timeline.update { currentTimeline ->
            currentTimeline.map { item ->
                if (item is TimelineItem.LiveTerminalItem && item.sessionId == sessionId) {
                    item.copy(exitCode = exitCode, executionTimeMs = executionTimeMs)
                } else {
                    item
                }
            }
        }

        // Also notify any waiting coroutines via the session result channel
        sessionResultChannels[sessionId]?.let { channel ->
            // Check cancelledByUser first to handle cancelled commands with exit code 0
            val result = when {
                cancelledByUser -> {
                    // User cancelled - include output in the message
                    val errorMessage = buildString {
                        appendLine("⚠️ Command cancelled by user")
                        appendLine()
                        appendLine("Exit code: $exitCode (SIGKILL)")
                        appendLine()
                        if (!output.isNullOrEmpty()) {
                            appendLine("Output before cancellation:")
                            appendLine(output)
                        } else {
                            appendLine("(no output captured before cancellation)")
                        }
                    }
                    cc.unitmesh.agent.tool.ToolResult.Error(
                        message = errorMessage,
                        errorType = "CANCELLED_BY_USER",
                        metadata = mapOf(
                            "exit_code" to exitCode.toString(),
                            "execution_time_ms" to executionTimeMs.toString(),
                            "output" to (output ?: ""),
                            "cancelled" to "true"
                        )
                    )
                }
                exitCode == 0 -> {
                    cc.unitmesh.agent.tool.ToolResult.Success(
                        content = output ?: "",
                        metadata = mapOf(
                            "exit_code" to exitCode.toString(),
                            "execution_time_ms" to executionTimeMs.toString()
                        )
                    )
                }
                else -> {
                    cc.unitmesh.agent.tool.ToolResult.Error(
                        message = "Command failed with exit code: $exitCode\n${output ?: ""}",
                        errorType = cc.unitmesh.agent.tool.ToolErrorType.COMMAND_FAILED.code,
                        metadata = mapOf(
                            "exit_code" to exitCode.toString(),
                            "execution_time_ms" to executionTimeMs.toString(),
                            "output" to (output ?: "")
                        )
                    )
                }
            }
            channel.trySend(result)
            sessionResultChannels.remove(sessionId)
        }
    }

    /**
     * Await the result of an async shell session.
     * Used when the Agent needs to wait for a shell command to complete before proceeding.
     */
    override suspend fun awaitSessionResult(sessionId: String, timeoutMs: Long): cc.unitmesh.agent.tool.ToolResult {
        // Check if the session is already completed
        val existingItem = _timeline.value.find {
            it is TimelineItem.LiveTerminalItem && it.sessionId == sessionId
        } as? TimelineItem.LiveTerminalItem

        if (existingItem?.exitCode != null) {
            // Session already completed
            return if (existingItem.exitCode == 0) {
                cc.unitmesh.agent.tool.ToolResult.Success(
                    content = "",
                    metadata = mapOf(
                        "exit_code" to existingItem.exitCode.toString(),
                        "execution_time_ms" to (existingItem.executionTimeMs ?: 0L).toString()
                    )
                )
            } else {
                cc.unitmesh.agent.tool.ToolResult.Error(
                    message = "Command failed with exit code: ${existingItem.exitCode}",
                    metadata = mapOf(
                        "exit_code" to existingItem.exitCode.toString(),
                        "execution_time_ms" to (existingItem.executionTimeMs ?: 0L).toString()
                    )
                )
            }
        }

        // Create a channel to wait for the result
        val channel = kotlinx.coroutines.channels.Channel<cc.unitmesh.agent.tool.ToolResult>(1)
        sessionResultChannels[sessionId] = channel

        return try {
            kotlinx.coroutines.withTimeout(timeoutMs) {
                channel.receive()
            }
        } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
            sessionResultChannels.remove(sessionId)
            cc.unitmesh.agent.tool.ToolResult.Error("Session timed out after ${timeoutMs}ms")
        }
    }

    /**
     * Render an Agent-generated sketch block (chart, nanodsl, mermaid, etc.)
     * Adds the sketch block to the timeline for interactive rendering.
     */
    override fun renderAgentSketchBlock(
        agentName: String,
        language: String,
        code: String,
        metadata: Map<String, String>
    ) {
        _timeline.update { current ->
            current + TimelineItem.AgentSketchBlockItem(
                agentName = agentName,
                language = language,
                code = code,
                metadata = metadata
            )
        }
    }
}

